package it.poli.IA.TilesGame;

import it.poli.IA.GlobalConvenctionValues;
import it.poli.IA.exceptions.IndexOutOfBoundCheckException;
import it.poli.IA.exceptions.NullActionException;
import it.poli.IA.representation.Position;
import it.poli.IA.space.Action;
import it.poli.IA.space.State;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;


public class TileState extends State implements Cloneable {
	private int [][]eightMatrix;
	
	private int colsNumber;
	private int rowsNumber;
	
	private final static int X=0;

	private final static int Y=1;

	public static boolean useSumOfDistance;

	
	/**
	 * leave null eigthMatrix and emptyTileposition to set a 3x3 matrix with random values in it
	 * @param eightMatrix
	 * @param emptyTileposition
	 */
	public TileState(int [][]eightMatrix)
	{
		if(eightMatrix == null)
		{
			eightMatrix = new int[3][3];
			this.rowsNumber = eightMatrix[0].length;
			this.colsNumber = rowsNumber;
			this.fillWithRandomValues();
			
		}
		else
		{
			this.rowsNumber = eightMatrix[0].length;
			this.colsNumber = rowsNumber;
			this.eightMatrix  = eightMatrix;
		
		}
	}
	
	
	/**
	 * Swap of element in position empty and element left, or up or down, or right
	 * @throws CloneNotSupportedException 
	 */
	@Override
	public  State getResult(Action a) throws CloneNotSupportedException 
	{
		
		Position deltaXY= null;
		try {
			deltaXY = this.deltaXYPosition(a);
		} 
		catch (NullActionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		TileState currState = this.getClone();
		
		return currState.getSwapped(deltaXY);
	}
	
	@Override
	public  boolean isAppliable(Action a ) throws NullActionException  {
		int[] delta = deltaXY(a);
		
		try
		{
			this.getElementRespectEmpty(new Position(delta[X], delta[Y]));
		}
		catch(IndexOutOfBoundCheckException e)
		{
			return false;
		}
		
		return true;
		
		
	}
	
	public void performListOfMoves(List<Action> actList)
	{
		for(Action a: actList)
			this.performMove(a);
	}
	
	public void performRandomMoves(int howMany)
	{
		for(int i=0;i<howMany; i++)
		{
			Random gen=new Random();
			int roll=gen.nextInt(4)+1;
			if (roll==1)
					performMove(new TileAction( TileActionEnum.up));
			else if (roll==2)
				performMove(new TileAction( TileActionEnum.down));
			else if (roll==3)
				performMove(new TileAction( TileActionEnum.left));
			else if (roll==4)
				performMove(new TileAction( TileActionEnum.right));
		
			
		}
		
	}
	
	
	public void performMove(Action a)
	{
		try 
		{
			if (isAppliable(a))
			{
				TileState s = (TileState) this.getResult(a);
				this.eightMatrix = s.eightMatrix;
				this.colsNumber = s.colsNumber;
				this.rowsNumber = s.rowsNumber;
				
			}
		} 
		catch (NullActionException e)
		{
			return;
		} 
		catch (CloneNotSupportedException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		catch(Exception e)
		{
			return ;
		}
	}
	
	
	public  TileState getSameDimensionGoalState()
	{
		
		int C= this.colsNumber; 
		int R=this.rowsNumber;
		int[][] goalSquare = new int[R][C];
		
		for(int r=0; r<R; r++)
			for(int c=0; c<C; c++)
			{
				goalSquare[r][c] = r*R + c + 1;
			}
		
		goalSquare[R-1][C-1]=0;
		TileState goalState = new TileState(goalSquare);
		
		return goalState;
		
		
	}

	
	
	private  Position deltaXYPosition(Action a) throws NullActionException {
		try 
		{
			return castDeltaToPosition(deltaXY(a));
		} 
		catch (NullActionException e) {
			throw new NullActionException();
		}
	}
	
	
	
	private void fillWithRandomValues() 
	{
		List<Integer> values = new ArrayList<Integer>();
		for(int i=0; i< this.colsNumber * this.rowsNumber -1 ; i++)
		{
			values.add(i+1);
		}
		values.add(GlobalConvenctionValues.EMPTY_TILE);
		
		Collections.shuffle(values);
		
		eightMatrix = new int[this.colsNumber][this.rowsNumber];
		
		for(int r=0; r<this.colsNumber; r++)
			for(int c=0; c<this.rowsNumber; c++)
			{
				eightMatrix[r][c]= values.get(r*this.rowsNumber + c);
			}
		
	}

	//TODO? find incrementally: state keep track of where was the empty tile and get an action to move it
	private int[] getEmptyTileCoords()
	{
		int[] position = new int[2];
		for(int r=0; r<this.colsNumber; r++)
			for(int c=0; c<this.rowsNumber; c++)
			{
				if(eightMatrix[r][c]== GlobalConvenctionValues.EMPTY_TILE)
				{
					position[GlobalConvenctionValues.X] = c;
					position[GlobalConvenctionValues.Y] = r;
				}
			}
		return position;
	}
	
	private Position getEmptyTilePosition()
	{
		int [] C = this.getEmptyTileCoords();
		return new Position(C[X], C[Y]);
	}

	
	private int getElementRespectEmpty(int deltaX, int deltaY) throws IndexOutOfBoundCheckException
	{
		Position position;
		position = this.getEmptyTilePosition();
		int emptyX = position.getX();
		int emptyY = position.getY();
		
		try
		{
			return this.eightMatrix[emptyX + deltaX][emptyY + deltaY];
		}
		catch(IndexOutOfBoundsException e)
		{
			throw new IndexOutOfBoundCheckException();
		}
	}
	
	
	protected int numberOfMisplacedTiles()
	{
		int cont = 0,r=0,c=0;
		for ( r=0; r<this.rowsNumber; r++)
			for ( c=0; c<this.colsNumber; c++)
			{
				if ( this.eightMatrix[r][c] != r*this.rowsNumber + c + 1)
					cont++;
			}
		if(this.eightMatrix[r-1][c-1] == 0)
			cont --;
		return cont;
	}
	

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Arrays.hashCode(eightMatrix);
		return result;
	}

	@Override
	public boolean equals(Object other) {
		if (this == other)
		{
			return true;
		}
		if (other == null) 
		{
			return false;
		}
		if (!(other instanceof TileState)) 
		{
			return false;
		}
		
		int i,j;
		TileState otherState = (TileState) other;
		for(i=0; i<colsNumber; i++)
		{	for(j=0; j<rowsNumber; j++)
			{
				if(this.eightMatrix[i][j] != otherState.eightMatrix[i][j])
				{
					return false;
				}
			}
		}
		return true;
	}

	@Override
	public String toString() {
		String s ="State :  ";
		s = s +  "\n      _______          \n";
		
		for(int i=0; i<this.rowsNumber; i++)
			s = s + "       " +  Arrays.toString(  this.eightMatrix[i])  + "\n";
		
		s = s +  "      _______          ";
		return s;
	}
	private void getElementRespectEmpty(Position position) throws IndexOutOfBoundCheckException {
		this.getElementRespectEmpty(position.getX(), position.getY());
		
	}
	
	
	private TileState getSwapped(Position deltaXY)
	{
	
		int [] C = this.getEmptyTileCoords();
		int empty_x = C[X];
		int empty_y = C[Y];
		
		Position newEmptyPos = new Position(empty_x, empty_y);
		
		Position target = newEmptyPos.getIncreasedXY(deltaXY);
		
		int target_x = target.getX();
		int target_y = target.getY();
		
		int[][] newEightMatrix = new int[this.rowsNumber][this.colsNumber];
		for(int i=0;i<this.rowsNumber; i++)
			for(int j=0;j<this.colsNumber; j++)
				newEightMatrix[i][j]= eightMatrix[i][j];
		
		int temp = this.eightMatrix[target_y][target_x];
		newEightMatrix[empty_y][empty_x] = temp;
		newEightMatrix[target_y][target_x] = GlobalConvenctionValues.EMPTY_TILE;
		
		newEmptyPos.setX(target_x);
		newEmptyPos.setY(target_y);
		
		TileState newState = new TileState(newEightMatrix);
		
		return newState;
		
		
	}

	private static Position castDeltaToPosition(int[] delta)
	{
		return new Position(delta[X], delta[Y]);
	}
	
	private static int[] deltaXY(Action a) throws NullActionException
	{
		int [] delta = new int[2];
		if(a==null)
		{
			throw new NullActionException();
		}
		switch ((TileActionEnum)a.actionEnumSelected) 
		{
			case down:
			{	
				delta[X]=0;
				delta[Y]=1;
				break;
			}
			case up:
			{	
				delta[X]=0;
				delta[Y]=-1;
				break;
			}
			case left:
			{
				delta[X]=-1;
				delta[Y]=0;
				break;
			}
			case right:
			{
				delta[X]=1;
				delta[Y]=0;
				break;
			}
	
			default:
				break;
		}
		return delta;
		
	}
	
	
	
	private TileState getClone() throws CloneNotSupportedException
	{
		return (TileState) this.clone();
	}


	@Override
	public Integer cost( Action a) {
		return this.numberOfMisplacedTiles();
	}


	public double getSumOfDistance(TileState givenState) {
		
		double distance =0;
		for(int r=0;r<this.rowsNumber;r++)
			for(int c=0; c<this.colsNumber; c++)
			{
				Position p1 = new Position(r,c);
				Position p2 = this.getCorrectPositionOf(givenState.eightMatrix[r][c]);
				distance += p1.distance(p2);
			}
		
		return distance;
		
		
	}

	// 1 2 3
	// 4 5 6
	// 7 8 0
	
	private Position getCorrectPositionOf(int i) {
		int x,y;
		if(i==0)
		{
			x= this.rowsNumber ;
			y = this.colsNumber ;
			return new Position(x-1, y-1);
		}
		
		 y = (int) Math.ceil( (double) i / (double) this.rowsNumber );
		 x = i % this.rowsNumber;
		 if(x==0)
			 x=this.rowsNumber;
		return new Position(y-1, x-1);
	}
	
	
	
}
